/*
 * Image.c : read image (PNG, JPEG or BMP) from memory or from disk.
 *
 * Written by T.Pierron, Apr 2010.
 */

#define GDIPLUS_DEF
#include "platform.h"
#include <stdio.h>
#include <stdlib.h>
#include <jpeglib.h>
#include <jerror.h>
#include <setjmp.h>
#include <string.h>
#include <malloc.h>
#include <png.h>
#include "graphics_P.h"
#include "image.h"

declPtr(jpeg_resync_to_restart);
declPtr(jpeg_std_error);
declPtr(jpeg_stdio_src);
declPtr(jpeg_destroy_decompress);
declPtr(jpeg_CreateDecompress);
declPtr(jpeg_read_header);
declPtr(jpeg_start_decompress);
declPtr(jpeg_read_scanlines);
declPtr(jpeg_finish_decompress);
declPtr(jpeg_resync_to_restart);

#define	jpeg_CreateDecompress pjpeg_CreateDecompress

declPtr(png_create_read_struct);
declPtr(png_create_info_struct);
declPtr(png_destroy_read_struct);
declPtr(png_init_io);
declPtr(png_read_info);
declPtr(png_get_IHDR);
declPtr(png_set_read_fn);
declPtr(png_get_io_ptr);
declPtr(png_set_palette_to_rgb);
declPtr(png_set_gray_1_2_4_to_8);
declPtr(png_get_valid);
declPtr(png_set_tRNS_to_alpha);
declPtr(png_set_strip_16);
declPtr(png_read_update_info);
declPtr(png_read_end);
declPtr(png_destroy_read_struct);
declPtr(png_read_rows);

#if 0
static GdiplusStartupProc pGdiplusStartup;
static GdipCreateBitmapFromStreamProc pGdipCreateBitmapFromStream;
static GdipDisposeImageProc pGdipDisposeImage;
static GdipBitmapLockBitsProc pGdipBitmapLockBits;
static GdipBitmapUnlockBitsProc pGdipBitmapUnlockBits;
HANDLE gdiplus
#endif

static int planes[] = {8, 24, 24, 32, 32};

HANDLE jpeg, png;

/* Dynamicly load what we need */
void IMG_Init(void)
{
	HANDLE dll;
	jpeg = LoadLibrary(L"jpeg62.dll");
	png  = LoadLibrary(L"libpng13.dll");

	if (! png) png = LoadLibrary(L"libpng12.dll");

	if (jpeg)
	{
		dll = jpeg;
		openPtr(jpeg_resync_to_restart);
		openPtr(jpeg_resync_to_restart);
		openPtr(jpeg_std_error);
		openPtr(jpeg_destroy_decompress);
		openPtr(jpeg_CreateDecompress);
		openPtr(jpeg_read_header);
		openPtr(jpeg_start_decompress);
		openPtr(jpeg_read_scanlines);
		openPtr(jpeg_finish_decompress);
		openPtr(jpeg_stdio_src);
	}

	if (png)
	{
		dll = png;
		openPtr(png_create_read_struct);
		openPtr(png_create_info_struct);
		openPtr(png_destroy_read_struct);
		openPtr(png_init_io);
		openPtr(png_read_info);
		openPtr(png_get_IHDR);
		openPtr(png_set_palette_to_rgb);
		openPtr(png_set_gray_1_2_4_to_8);
		openPtr(png_get_valid);
		openPtr(png_set_tRNS_to_alpha);
		openPtr(png_set_strip_16);
		openPtr(png_read_update_info);
		openPtr(png_read_end);
		openPtr(png_destroy_read_struct);
		openPtr(png_read_rows);
		openPtr(png_set_read_fn);
		openPtr(png_get_io_ptr);
	}

	#if 0
	/* If png and jpeg libraries are not available, try gdi+ */
	if (!png && !jpeg && (gdiplus = LoadLibrary(L"gdiplus.dll")))
	{
		ULONG_PTR token;
		GdiplusStartupOutput gpOutput;

		/* Try with gdiplus */
		pGdiplusStartup             = (APTR) GetProcAddress(gdiplus, "GdiplusStartup");
		pGdipCreateBitmapFromStream = (APTR) GetProcAddress(gdiplus, "GdipCreateBitmapFromStream" /*"ICM"*/);
		pGdipDisposeImage           = (APTR) GetProcAddress(gdiplus, "GdipDisposeImage");
		pGdipBitmapLockBits         = (APTR) GetProcAddress(gdiplus, "GdipBitmapLockBits");
		pGdipBitmapUnlockBits       = (APTR) GetProcAddress(gdiplus, "GdipBitmapUnlockBits");

		GdiplusStartupInput gpStartupInput = {1, NULL, FALSE, FALSE};
		if (pGdiplusStartup(&token, &gpStartupInput, &gpOutput) != 0) {
			FreeLibrary(gdiplus);
			gdiplus = 0;
		}
	}
	#endif
}

static void png_read_cb(png_structp png, png_bytep data, png_size_t length)
{
	png_read * io = ppng_get_io_ptr(png);
	int        sz = length;

	if (sz + io->pos > io->length)
		sz = io->length - io->pos;

	memcpy(data, io->stream + io->pos, sz);
	io->pos += sz;
}

#ifdef DEBUG
static void png_error_cb(png_structp png, png_const_charp msg)
{
	fprintf(stderr, "error: %s\n", msg);
}
#else
#define	png_error_cb  NULL
#endif

static Image IMG_ReadPNG(FILE * in, DATA8 stream, int length, int mode)
{
	png_structp png;
	png_infop   info;
	png_uint_32 width, height;
	png_read    io;
	png_bytep   p;
	int         bpp, spp, cspace, i, j;
	Image       ret = NULL;

	if (! ppng_create_read_struct) return NULL;

	png = ppng_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_cb, png_error_cb);

	if (png == NULL) return NULL;

	info = ppng_create_info_struct(png);
	if (info == NULL)
	{
		ppng_destroy_read_struct(&png, NULL, NULL);
		return NULL;
	}

	if (setjmp(png_jmpbuf(png)))
	{
		if (ret) free(ret);
		ppng_destroy_read_struct(&png, &info, NULL);
		return NULL;
	}

	io.stream = stream;
	io.length = length;
	io.pos    = 0;

	if (in) ppng_init_io(png, in);
	else    ppng_set_read_fn(png, &io, png_read_cb);
	ppng_read_info(png, info);
	ppng_get_IHDR(png, info, &width, &height, &bpp, &cspace, NULL, NULL, NULL);

	/* Apply filters to image so we can get proper image data format */
	if (cspace == PNG_COLOR_TYPE_PALETTE)
		ppng_set_palette_to_rgb(png), cspace = PNG_COLOR_TYPE_RGB;
	if (cspace == PNG_COLOR_TYPE_GRAY && bpp < 8)
		ppng_set_gray_1_2_4_to_8(png);
	if (ppng_get_valid(png, info, PNG_INFO_tRNS))
		ppng_set_tRNS_to_alpha(png);
	if (bpp == 16)
		ppng_set_strip_16(png), bpp = 8; /* Force 8-bit */

	switch (cspace) {
	case PNG_COLOR_TYPE_GRAY: spp = 1; break;
	case PNG_COLOR_TYPE_GRAY_ALPHA: spp = 2; break;
	case PNG_COLOR_TYPE_RGB: spp = 3; break;
	case PNG_COLOR_TYPE_RGB_ALPHA: spp = 4; break;
	default: spp = 0;
	}

	j = (width * spp + 3) & ~3;
	ret = malloc(sizeof *ret + ((mode & 5) || spp == 4 ? 0 : height * j));

	ret->width   = width;
	ret->height  = height;
	ret->format  = mode & 2 ? (spp == 3 ? GFX_FormatRGB : GFX_FormatRGBA) :
	                          (spp == 3 ? GFX_FormatBGR : GFX_FormatABGR) ;
	ret->bpp     = planes[ret->format];
	ret->encoder = GFX_EncoderPNG;
	ret->stride  = width = j;

	if (spp == 1) ret->format = GFX_FormatK;

	/* For images with alpha channel, let GDI alloc the memory so that we can use AlphaBlend */
	if (spp == 4 || (mode & 4))
	{
		BITMAPINFO info = {};
		info.bmiHeader.biSize = sizeof info.bmiHeader;
		info.bmiHeader.biWidth = ret->width;
		info.bmiHeader.biHeight = - height;
		info.bmiHeader.biPlanes = 1;
		info.bmiHeader.biBitCount = ret->bpp;
		ret->hbitmap = CreateDIBSection(NULL, &info, DIB_RGB_COLORS, (APTR)&ret->bitmap, 0, 0);
	}
	else ret->bitmap = (APTR) (ret + 1), ret->hbitmap = NULL;

	if ((mode & 1) == 0)
	{
		for (i = 0, p = ret->bitmap; i < height; i ++, p += width)
		{
			png_bytep row = p;
			ppng_read_rows(png, &row, NULL, 1);
			if (mode & 2) continue;
			switch (spp) {
			case 3:
				for (row = p, j = ret->width; j > 0; j --, row += 3)
					swap(row[0], row[2]);
				break;
			case 4:
				ret->encoder |= GFX_Premultiplied;
				for (row = p, j = width; j > 0; j -= 4, row += 4)
				{
					swap(row[0], row[2]);
					/* Pre-multiply */
					row[0] = row[0] * row[3] / 255;
					row[1] = row[1] * row[3] / 255;
					row[2] = row[2] * row[3] / 255;
				}
			}
		}
	}
	ppng_read_end(png, info);
	ppng_destroy_read_struct(&png, &info, NULL);
	return ret;
}

/*
 * Read a JPEG file, using custom datasource. Derived from jdatasrc.c from libjpeg src.
 */
static void init_source(j_decompress_ptr cinfo)
{
	jpeg_source src = (APTR) cinfo->src;

	src->first = True;
}

static boolean fill_input_buffer(j_decompress_ptr cinfo)
{
	jpeg_source src = (APTR) cinfo->src;

	if (! src->first)
	{
		/* Insert a fake EOI marker */
		src->buffer[0] = 0xFF;
		src->buffer[1] = JPEG_EOI;
		src->max = 2;
	}
	src->pub.next_input_byte = src->buffer;
	src->pub.bytes_in_buffer = src->max;
	src->first = False;

	return True;
}

static void no_op()
{
}
/*
static void jpeg_error_exit(j_common_ptr cinfo)
{
	jpeg_error err = (jpeg_error) cinfo->err;
	(*cinfo->err->output_message) (cinfo);
	longjmp(err->setjmp_buffer, 1);
}
*/

static void jpeg_memory_src(j_decompress_ptr cinfo, jpeg_source src, DATA8 buffer, ULONG size)
{
	cinfo->src = &src->pub;
	src->pub.init_source = init_source;
	src->pub.fill_input_buffer = fill_input_buffer;
	src->pub.skip_input_data = no_op;
	src->pub.resync_to_restart = pjpeg_resync_to_restart; /* use default method */
	src->pub.term_source = no_op;
	src->buffer = buffer;
	src->max = size;
	src->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */
	src->pub.next_input_byte = NULL; /* until buffer loaded */
}

static Image IMG_ReadJPEG(FILE * in, DATA8 stream, int length, int mode)
{
	struct jpeg_decompress_struct cinfo;
	struct jpeg_error_mgr jerr;
	struct jpeg_source src;
	int    stride, width, i;
	Image  ret = NULL;
	DATA8  rowptr;

	if (! pjpeg_std_error) return NULL;

	cinfo.err = pjpeg_std_error(&jerr);
	jpeg_create_decompress(&cinfo);
	if (in) pjpeg_stdio_src(&cinfo, in);
	else    jpeg_memory_src(&cinfo, &src, stream, length);

	pjpeg_read_header(&cinfo, True);
	pjpeg_start_decompress(&cinfo);

	stride = cinfo.output_width * cinfo.output_components;
	width  = (stride + 3) & ~3;

	if (mode & 4)
	{
		/* DIB */
		BITMAPINFO info = {};
		ret = calloc(sizeof *ret, 1);
		info.bmiHeader.biSize = sizeof info.bmiHeader;
		info.bmiHeader.biWidth = cinfo.output_width;
		info.bmiHeader.biHeight = - cinfo.output_height;
		info.bmiHeader.biPlanes = 1;
		info.bmiHeader.biBitCount = cinfo.output_components << 3;
		ret->hbitmap = CreateDIBSection(NULL, &info, DIB_RGB_COLORS, (APTR)&ret->bitmap, 0, 0);
	}
	else /* Only one pointer to free */
	{
		ret = malloc(sizeof *ret + (mode & 1 ? 0 : cinfo.output_height * width));
		memset(ret, 0, sizeof *ret);
	}
	ret->width   = cinfo.output_width;
	ret->height  = cinfo.output_height;
	ret->encoder = GFX_EncoderJPEG;
	ret->stride  = width;
	ret->format  = cinfo.output_components == 1 ? GFX_FormatK : GFX_FormatBGR;
	ret->bpp     = cinfo.output_components << 3;

	if ((mode & 1) == 0)
	{
		ret->bitmap = rowptr = (APTR) (ret + 1);

		for (; cinfo.output_scanline < ret->height; rowptr += width)
		{
			DATA8 p;
			if (pjpeg_read_scanlines(&cinfo, &rowptr, 1) < 1)
			{
				free(ret); ret = NULL;
				break;
			}
			if (mode & 2) continue;
			for (p = rowptr, i = 0; i + 2 < width; i += 3, p += 3)
				swap(p[0], p[2]);
		}
	}
	pjpeg_finish_decompress(&cinfo);
	pjpeg_destroy_decompress(&cinfo);

	return ret;
}

/* Read from inline string (BMP not supported) */
Image IMG_ReadFromBuf(DATA8 stream, int length, int mode)
{
	if (memcmp(stream, "\xff\xd8\xff\xe0", 4) == 0)
	{
		return IMG_ReadJPEG(NULL, stream, length, mode);
	}
	else if (memcmp(stream, "\x89PNG", 4) == 0)
	{
		return IMG_ReadPNG(NULL, stream, length, mode);
	}
	else return NULL; // BMP are supposed to be in ressource fork
}

static Image IMG_CreateFromBMP(HBITMAP hbmp)
{
	BITMAP bmp;
	Image  info = NULL;

	if (GetObject(hbmp, sizeof bmp, &bmp) > 0)
	{
		info = malloc(sizeof *info);
		info->width   = bmp.bmWidth;
		info->height  = bmp.bmHeight;
		info->stride  = bmp.bmWidthBytes;
		info->encoder = GFX_EncoderBMP;
		info->bitmap  = bmp.bmBits;
		info->hbitmap = hbmp;
		info->palette = NULL;
		info->format  = bmp.bmBitsPixel > 24 ? GFX_FormatABGR : GFX_FormatBGR;
		info->bpp     = planes[info->format];
		if (info->bitmap && info->bpp == 32)
		{
			info->encoder |= GFX_Premultiplied;
			DATA8 row; int j;
			for (row = info->bitmap, j = info->width * info->height; j > 0; j --, row += 4)
			{
				row[0] = row[0] * row[3] / 255;
				row[1] = row[1] * row[3] / 255;
				row[2] = row[2] * row[3] / 255;
			}
		}
	}
	return info;
}

/* Read from resource fork */
Image IMG_ReadFromRes(STRPTR res)
{
	LPWSTR wres, p;
	int id, type, len = MultiByteToWideChar(CP_UTF8, 0, res, -1, NULL, 0);

	wres = alloca(len * sizeof *wres);
	MultiByteToWideChar(CP_UTF8, 0, res, -1, (LPWSTR) wres, len);
	p = wcschr(wres, '/');
	if (p)
	{
		// Usually PNG or JPEG
		HRSRC hr;
		*p++ = 0;
		if (sscanf(res, "%d/%d", &id, &type) == 2)
			hr = FindResource(NULL, (LPTSTR) id, (LPTSTR) type);
		else
			hr = FindResource(NULL, wres, p);

		if (hr)
		{
			HGLOBAL hptr = LoadResource(NULL, hr);
			DATA8   ptr  = LockResource(hptr);

			/* No need to free resource (or actually there is no API to free memory) */
			return IMG_ReadFromBuf(ptr, SizeofResource(NULL, hr), 0);
		}
	}
	else /* BMP */
	{
		extern APTR instance;
		HBITMAP hbmp;
		hbmp = LoadImage(instance, sscanf(res, "%d", &id) == 1 ? MAKEINTRESOURCE(id) : wres,
			IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_DEFAULTSIZE);

		if (hbmp) return IMG_CreateFromBMP(hbmp);
	}
	return NULL;
}

/* Read a JPEG, PNG or BMP image */
Image IMG_Read(STRPTR path, int mode)
{
	FILE * in;
	Image  info = NULL;
	TEXT   header[4];

	if (path == NULL) return 0; in = fopen_enc(path, "rb");
	if (in   == NULL) return 0;

	fread(header, 1, sizeof header, in);
	fseek(in, 0, SEEK_SET);

/*	if (gdiplus)
	{
		info = IMG_ReadGdip(in, NULL, 0, mode);
	}
	else*/ if (memcmp("\xff\xd8", header, 2) == 0)
	{
		info = IMG_ReadJPEG(in, NULL, 0, mode);
	}
	else if (memcmp("\x89PNG", header, 4) == 0)
	{
		info = IMG_ReadPNG(in, NULL, 0, mode);
	}
	else if (memcmp("BM", header, 2) == 0)
	{
		/* Well, just do the bare minimum */
		HBITMAP hbmp;
		int     len = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
		LPWSTR  wpath = alloca(len * sizeof *wpath);

		fclose(in);
		MultiByteToWideChar(CP_UTF8, 0, path, -1, (LPWSTR) wpath, len);
		hbmp = LoadImage(NULL, wpath, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION|LR_LOADFROMFILE);
		if (hbmp) return IMG_CreateFromBMP(hbmp);
	}
	fclose(in);
	return info;
}
